function LineMotion(x1, x2, t) {
    return x1.value + (x2.value - x1.value) * t;
}

var t2 = 0;
var t3 = 0;
function SplineMotion(x1, x2, r1, r2, t) {
    t2 = t * t;
    t3 = t2 * t;
    return x1.value * (2.0 * t3 - 3.0 * t2 + 1.0) + r1.gradient * (t3 - 2.0 * t2 + t) + x2.value * (-2.0 * t3 + 3.0 * t2) + r2.gradient * (t3 - t2);
}

var motion_line = 1;
var motion_spline = 2;
var motion_discontinuous = 3;

function MotionValues() {

    this._type;

    this.keys = [];

    this.start;
    this.end;
    this.middle;

    this.Clear = function () {
        this.keys.length = 0;
    };

    this.AddKey = function (time, value) {
        var key = {};
        key.time = +time;
        key.value = +value;
        this.keys.push(key);
        return time;
    };

    this.SetType = function (type) {
        this._type = type;

        if (this.keys.length == 1) {
            this.AddKey(1.0, this.keys[0].value);
        }

        if (type == motion_spline) {

            this.keys[0].gradient = this.keys[1].value - this.keys[0].value;
            this.keys[this.keys.length - 1].gradient =
                this.keys[this.keys.length - 1].value - this.keys[this.keys.length - 2].value;

            var g1, g2, g3;
            for (var i = 1; i < (this.keys.length - 1); i++) {
                g1 = this.keys[i].value - this.keys[i - 1].value;
                g2 = this.keys[i + 1].value - this.keys[i].value;
                g3 = g2 - g1;
                this.keys[i].gradient = g1 + 0.5 * g3;
            }

        }
    };

    return this;
}


function Value(t, obj) {

    var cord = obj._x;

    if (cord.keys[0].time > t || t > cord.keys[cord.keys.length - 1].time) {
        return -1;
    }

    cord.start = 0;
    cord.end = cord.keys.length - 2;
    cord.middle = Math.floor((cord.start + cord.end) / 2);
    while (!(cord.keys[cord.middle].time <= t && t <= cord.keys[cord.middle + 1].time)) {
        if (cord.keys[cord.middle].time > t) {
            cord.end = cord.middle;
        } else if (cord.keys[cord.middle + 1].time < t) {
            cord.start = cord.middle + 1;
        }
        cord.middle = Math.floor((cord.start + cord.end) / 2);
    }

    obj.localT = (t - cord.keys[cord.middle].time) / (cord.keys[cord.middle + 1].time - cord.keys[cord.middle].time);
    return cord.middle;
}

function GetFrame(obj, i, t) {
    if (obj._type == motion_line) {
        return LineMotion(obj.keys[i], obj.keys[i + 1], t);
    } else if (obj._type == motion_discontinuous) {
        return obj.keys[i].value;
    } else {
        return SplineMotion(obj.keys[i], obj.keys[i + 1],
            obj.keys[i], obj.keys[i + 1], t);
    }
}


function Point2D() {
    this.x = 0;
    this.y = 0;
    return this;
}

function Read(xe, name, defaultValue) {
    var tmp = xe.attributes.getNamedItem(name);
    return (tmp ? +tmp.value : defaultValue);
}


function Matrix() {

    this._matrix = [
        [1,0,0],
        [0,1,0],
        [0,0,1]
    ];

    this.MulM = function (m) {
        var m00 = m[0][0],
            m01 = m[0][1],
            m02 = m[0][2],
            m10 = m[1][0],
            m11 = m[1][1],
            m12 = m[1][2],
            m20 = m[2][0],
            m21 = m[2][1],
            m22 = m[2][2],

            n00 = this._matrix[0][0],
            n01 = this._matrix[0][1],
            n02 = this._matrix[0][2],
            n10 = this._matrix[1][0],
            n11 = this._matrix[1][1],
            n12 = this._matrix[1][2],
            n20 = this._matrix[2][0],
            n21 = this._matrix[2][1],
            n22 = this._matrix[2][2];

        this._matrix[0][0] = m00 * n00 + m01 * n10 + m02 * n20;
        this._matrix[0][1] = m00 * n01 + m01 * n11 + m02 * n21;
        this._matrix[0][2] = m00 * n02 + m01 * n12 + m02 * n22;
        this._matrix[1][0] = m10 * n00 + m11 * n10 + m12 * n20;
        this._matrix[1][1] = m10 * n01 + m11 * n11 + m12 * n21;
        this._matrix[1][2] = m10 * n02 + m11 * n12 + m12 * n22;
        this._matrix[2][0] = m20 * n00 + m21 * n10 + m22 * n20;
        this._matrix[2][1] = m20 * n01 + m21 * n11 + m22 * n21;
        this._matrix[2][2] = m20 * n02 + m21 * n12 + m22 * n22;
    };

    this.Unit = function () {
        this._matrix[0][0] = 1;
        this._matrix[0][1] = 0;
        this._matrix[0][2] = 0;
        this._matrix[1][0] = 0;
        this._matrix[1][1] = 1;
        this._matrix[1][2] = 0;
        this._matrix[2][0] = 0;
        this._matrix[2][1] = 0;
        this._matrix[2][2] = 1;
    };

    this.Assign = function (m) {
        this._matrix[0][0] = m._matrix[0][0];
        this._matrix[0][1] = m._matrix[0][1];
        this._matrix[0][2] = m._matrix[0][2];
        this._matrix[1][0] = m._matrix[1][0];
        this._matrix[1][1] = m._matrix[1][1];
        this._matrix[1][2] = m._matrix[1][2];
        this._matrix[2][0] = m._matrix[2][0];
        this._matrix[2][1] = m._matrix[2][1];
        this._matrix[2][2] = m._matrix[2][2];
    };


    var tmp = [
        [],
        [],
        []
    ];

    this.Rotate = function (angle) {
        tmp[0][0] = Math.cos(angle);
        tmp[0][1] = Math.sin(angle);
        tmp[0][2] = 0;
        tmp[1][0] = -Math.sin(angle);
        tmp[1][1] = Math.cos(angle);
        tmp[1][2] = 0;
        tmp[2][0] = 0;
        tmp[2][1] = 0;
        tmp[2][2] = 1;
        this.MulM(tmp);
    };

    this.Scale = function (scx, scy) {
        tmp[0][0] = scx;
        tmp[0][1] = 0;
        tmp[0][2] = 0;
        tmp[1][0] = 0;
        tmp[1][1] = scy;
        tmp[1][2] = 0;
        tmp[2][0] = 0;
        tmp[2][1] = 0;
        tmp[2][2] = 1;
        this.MulM(tmp);
    };

    this.Move = function (x, y) {
        tmp[0][0] = 1.0;
        tmp[0][1] = 0.0;
        tmp[0][2] = 0;
        tmp[1][0] = 0.0;
        tmp[1][1] = 1.0;
        tmp[1][2] = 0;
        tmp[2][0] = x;
        tmp[2][1] = y;
        tmp[2][2] = 1;
        this.MulM(tmp);
    };

    this.MulMatrix = function (transform) {
        this.MulM(transform._matrix);
    };

    this.MulXY = function (sx, sy, x, y) {
        x[0] = this._matrix[0][0] * sx + this._matrix[1][0] * sy + this._matrix[2][0];
        y[0] = this._matrix[0][1] * sx + this._matrix[1][1] * sy + this._matrix[2][1];
    };

    this.Determinant = function () {
        return this._matrix[0][0] * this._matrix[1][1] * this._matrix[2][2]
            - this._matrix[0][0] * this._matrix[1][2] * this._matrix[2][1]
            - this._matrix[0][1] * this._matrix[1][0] * this._matrix[2][2]
            + this._matrix[0][1] * this._matrix[1][2] * this._matrix[2][0]
            + this._matrix[0][2] * this._matrix[1][0] * this._matrix[2][1]
            - this._matrix[0][2] * this._matrix[1][1] * this._matrix[2][0];
    };

    this.Flip = function () {
        var t;

        t = this._matrix[1][0];
        this._matrix[1][0] = this._matrix[0][1];
        this._matrix[0][1] = t;

        t = this._matrix[1][2];
        this._matrix[1][2] = this._matrix[2][1];
        this._matrix[2][1] = t;

        t = this._matrix[2][0];
        this._matrix[2][0] = this._matrix[0][2];
        this._matrix[0][2] = t;
    };

    this.Mul = function (f) {
        this._matrix[0][0] *= f;
        this._matrix[1][0] *= f;
        this._matrix[2][0] *= f;

        this._matrix[0][1] *= f;
        this._matrix[1][1] *= f;
        this._matrix[2][1] *= f;

        this._matrix[0][2] *= f;
        this._matrix[1][2] *= f;
        this._matrix[2][2] *= f;
    };

    this.MakeRevers = function (transform) {

        var det = transform.Determinant();

        this._matrix[0][0] = (transform._matrix[1][1] * transform._matrix[2][2] - transform._matrix[2][1] * transform._matrix[1][2]);
        this._matrix[1][0] = -(transform._matrix[0][1] * transform._matrix[2][2] - transform._matrix[2][1] * transform._matrix[0][2]);
        this._matrix[2][0] = (transform._matrix[0][1] * transform._matrix[1][2] - transform._matrix[1][1] * transform._matrix[0][2]);

        this._matrix[0][1] = -(transform._matrix[1][0] * transform._matrix[2][2] - transform._matrix[2][0] * transform._matrix[1][2]);
        this._matrix[1][1] = (transform._matrix[0][0] * transform._matrix[2][2] - transform._matrix[2][0] * transform._matrix[0][2]);
        this._matrix[2][1] = -(transform._matrix[0][0] * transform._matrix[1][2] - transform._matrix[1][0] * transform._matrix[0][2]);

        this._matrix[0][2] = (transform._matrix[1][0] * transform._matrix[2][1] - transform._matrix[2][0] * transform._matrix[1][1]);
        this._matrix[1][2] = -(transform._matrix[0][0] * transform._matrix[2][1] - transform._matrix[2][0] * transform._matrix[0][1]);
        this._matrix[2][2] = (transform._matrix[0][0] * transform._matrix[1][1] - transform._matrix[1][0] * transform._matrix[0][1]);

        this.Flip();

        this.Mul(1 / det);
    };

    this.ApplyTransform = function (context) {
        context.transform(this._matrix[0][0], this._matrix[0][1], this._matrix[1][0], this._matrix[1][1], this._matrix[2][0], this._matrix[2][1])
    };

    return this;
}


function MovingPart(animation, xe, texture) {

    this.boneName;
    this._center = new Point2D();
    this._x = new MotionValues();
    this._y = new MotionValues();
    this._angle = new MotionValues();
    this._scaleX = new MotionValues();
    this._scaleY = new MotionValues();
    this._movingType;

    this._bones = [];
    this._order;

    this._texture;
    this._center = new Point2D();
    // sprite's texture coords
    this._sx;
    this._sy;
    this._sw;
    this._sh;

    this._offsetx;
    this._offsety;
    this._visible = false;
    this._screenMatrix = new Matrix();

    this.MovingPart_constructor = function (animation, xe, texture) {
        animation.AddBone(this);
        this._texture = texture;
        this.boneName = xe.attributes.getNamedItem("name").value;

        var tmp = xe.attributes.getNamedItem("moving_type") && xe.attributes.getNamedItem("moving_type").value;
        if (!tmp || tmp == "spline") {
            this._movingType = motion_spline;
        } else if (tmp == "line") {
            this._movingType = motion_line;
        } else {
            this._movingType = motion_discontinuous;
        }

        this._order = +xe.attributes.getNamedItem("order").value;

        this._center.x = Read(xe, "centerX", 0);
        this._center.y = Read(xe, "centerY", 0);

        var description = xe.attributes.getNamedItem("texture").value;

        CreateQuad(texture, description, this);

        var poses = xe.getElementsByTagName("pos");
        if (poses.length == 0) {
            this._x.AddKey(0, 0);
            this._y.AddKey(0, 0);
            this._scaleX.AddKey(0, 1);
            this._scaleY.AddKey(0, 1);
            this._angle.AddKey(0, 0);
        }
        else {
            for (var i = 0; i < poses.length; ++i) {
                if (poses.item(i).parentNode == xe) {
                    var time = Read(poses.item(i), "time", 0);
                    this._x.AddKey(time, Read(poses.item(i), "x", 0));
                    this._y.AddKey(time, Read(poses.item(i), "y", 0));
                    this._scaleX.AddKey(time, Read(poses.item(i), "scaleX", 1));
                    this._scaleY.AddKey(time, Read(poses.item(i), "scaleY", 1));
                    this._angle.AddKey(time, Read(poses.item(i), "angle", 0));
                }
            }
        }

        this._x.SetType(this._movingType);
        this._y.SetType(this._movingType);
        this._scaleX.SetType(this._movingType);
        this._scaleY.SetType(this._movingType);
        this._angle.SetType(this._movingType);


        var elements = xe.getElementsByTagName("movingPart");
        for (var i = 0; i < elements.length; ++i) {
            if (elements.item(i).parentNode == xe) {
                this._bones.push(new MovingPart(animation, elements.item(i), texture));
            }
        }
    };

    this.localT = 0;

    this.PreDraw = function (p, stack) {
        var index = Value(p, this);

        if (this._visible = (index >= 0)) {
            this._screenMatrix.Assign(stack);
            this._screenMatrix.Move(GetFrame(this._x, index, this.localT), GetFrame(this._y, index, this.localT));
            this._screenMatrix.Rotate(GetFrame(this._angle, index, this.localT));
            this._screenMatrix.Scale(GetFrame(this._scaleX, index, this.localT), GetFrame(this._scaleY, index, this.localT));
            this._screenMatrix.Move(-this._center.x, -this._center.y);

            for (var i = 0; i < this._bones.length; ++i) {
                this._bones[i].PreDraw(p, this._screenMatrix);
            }


        }
    };

    this.Draw = function (context) {
        if (this._visible) {
            this._screenMatrix.ApplyTransform(context);
            context.drawImage(this._texture
                , this._sx, this._sy, this._sw, this._sh,
                0, 0, this._sw, this._sh);
        }
    };

    var CreateQuad = function (texture, description, obj) {

        obj._texture = texture;
        var desArr = description.split(":");

        obj._sx = +desArr[0];
        obj._sy = +desArr[1];
        obj._sw = +desArr[2];
        obj._sh = +desArr[3];
        obj._offsetx = +desArr[4];
        obj.offsety = +desArr[5];
    };

    this.MovingPart_constructor(animation, xe, texture);

    this.MovingPart_constructor = null;

    return this;
}


function CmpMovingPart(one, two) {
    return one._order - two._order;
}


function Animation(xe, texture) {

    this._pivotPos = new Point2D();
    this._bones = [];
    this._matrix = new Matrix();
    this._renderList = [];

    this.SetPos = function (x, y, mirror) {
        this._matrix.Unit();
        this._matrix.Move(x, y);
        if (mirror) {
            this._matrix.Scale(-1, 1);
        }
    };

    this.AddBone = function (bone) {
        this._renderList.push(bone);
    };

    this.Draw = function (context, position) {

        for (var i = 0; i < this._renderList.length; ++i) {
            this._renderList[i]._visible = false;
        }

        for (var i = 0; i < this._bones.length; ++i) {
            this._bones[i].PreDraw(position, this._matrix);
        }

        for (var i = 0; i < this._renderList.length; ++i) {
            context.save();
            this._renderList[i].Draw(context);
            context.restore();
            //context.setTransform(1, 0, 0, 1, 0, 0);
        }
    };

    this.Time = function () {
        return this._time;
    };

    this.position = 0;

    this.SetFPS = function (value) {
        this.fps = value;
    };

    this.Update = function () {
        this.position += 1 / this.fps / this._time;
        while (this.position > 1) {
            this.position -= 1;
        }
    };

    this.DrawAtPoint = function (context, x, y, mirror) {
        this.SetPos(x, y, mirror != "undefined" ? mirror : false);
        this.Draw(context, this.position);
    };

    this._time = +xe.attributes.getNamedItem("time").value;
    this._pivotPos.x = +xe.attributes.getNamedItem("pivotX").value;
    this._pivotPos.y = +xe.attributes.getNamedItem("pivotY").value;

    var elements = xe.getElementsByTagName("movingPart");
    for (i = 0; i < elements.length; ++i) {
        if (elements.item(i).parentNode == xe) {
            this._bones.push(new MovingPart(this, elements.item(i), texture));
        }
    }
    this._renderList.sort(CmpMovingPart);

    return this;
}


function loadXMLDoc(XMLname) {
    var xmlDoc;
    if (window.XMLHttpRequest) {
        xmlDoc = new window.XMLHttpRequest();
        xmlDoc.open("GET", XMLname, false);
        xmlDoc.send("");
        return xmlDoc.responseXML;
    }
    // IE 5 and IE 6
    else if (ActiveXObject("Microsoft.XMLDOM")) {
        xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
        xmlDoc.async = false;
        xmlDoc.load(XMLname);
        return xmlDoc;
    }
    alert("Error loading document!");
    return null;
}


function AnimationsCollection(name, onReady) {

    this.animations = [];
    this.callOnReady = onReady;
    this.fileName = name;

    this.load = function () {
        this.imageObj = new Image();
        this.imageObj.src = this.fileName + ".png";
        this.imageObj.onload = this.onImageLoad(this);
    };

    this.onImageLoad = function (obj) {
        var xmlDoc = loadXMLDoc(obj.fileName + ".xml");
        var elements = xmlDoc.getElementsByTagName("Animation");
        for (var i = 0; i < elements.length; ++i) {
            var name = elements.item(i).attributes.getNamedItem("id").value;
            obj.animations[name] = new Animation(elements.item(i), obj.imageObj);
        }
        obj.callOnReady();
    };

    this.get = function (id) {
        return this.animations[id];
    };

    return this;

}
